/**
 * Copyright (c) 2016 NumberFour AG.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *   NumberFour AG - Initial API and implementation
 */
package org.eclipse.n4js.parser;

import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.Optional;
import java.util.stream.Stream;

import org.antlr.runtime.BitSet;
import org.antlr.runtime.IntStream;
import org.antlr.runtime.MismatchedTokenException;
import org.antlr.runtime.NoViableAltException;
import org.antlr.runtime.RecognitionException;
import org.antlr.runtime.RecognizerSharedState;
import org.antlr.runtime.Token;
import org.antlr.runtime.TokenStream;
import org.eclipse.n4js.parser.antlr.internal.InternalN4JSParser;
import org.eclipse.n4js.services.N4JSGrammarAccess;
import org.eclipse.xtext.nodemodel.SyntaxErrorMessage;
import org.eclipse.xtext.parser.antlr.AbstractInternalAntlrParser;

/**
 * <p>
 * A customized {@link InternalN4JSParser} that adds behavior which is unique to the JS parser semantics. Especially
 * important is the error recovery which has to be aware of automatically injected semicolons.
 * </p>
 * This parser inherits the generated {@link InternalN4JSParser}, which already contains customized methods stubs for
 * enabling semicolon-insertion and regular-expression-handling. That is, instead of injecting fully implemented
 * methods, only stubs are inserted and the real implementation is found here. This simplifies programming, as we do not
 * have to "implement" the methods in some template w/o any Java validation support.
 */
public class InternalSemicolonInjectingParser extends InternalN4JSParser implements SemicolonInjectionHelper.Callback {

	private static final Field reflectCurrentError;

	static {
		Field f = null;
		try {
			f = AbstractInternalAntlrParser.class.getDeclaredField("currentError");
			f.setAccessible(true);
		} catch (Exception e) {
			// ignore
		}
		reflectCurrentError = f;
	}

	/**
	 * Issue code for syntactical occurrences of automatically inserted semicolon.
	 */
	public static final String SEMICOLON_INSERTED = "InternalSemicolonInjectingParser.ASI";

	private final RecoverySets recoverySets;

	private NoViableAltException asiRecoveredEx = null;

	@Override
	public boolean allowASI(final RecognitionException re) {
		if (re instanceof NoViableAltException) {
			final NoViableAltException nvae = (NoViableAltException) re;
			if (asiRecoveredEx != null && re.index == asiRecoveredEx.index
					&& nvae.decisionNumber == asiRecoveredEx.decisionNumber) {
				return false;
			}
			asiRecoveredEx = nvae;
		}
		return true;
	}

	/**
	 * @param input
	 *            token stream, is to be expected an {@link LazyTokenStream}
	 */
	InternalSemicolonInjectingParser(TokenStream input, N4JSGrammarAccess grammarAccess) {
		super(input, grammarAccess);
		recoverySets = computeRecoverySets();
	}

	/**
	 * Assign the regular expression mode to the {@link RegExLiteralAwareLexer lexer}. This method is called from
	 * {@link #ruleREGEX_LITERAL()}, which is injected into the generated Antlr parser by
	 * {@link org.eclipse.n4js.antlr.n4js.RegExDisambiguationInjector}, see MWE2 workflow for configuration.
	 * <p>
	 * Overrides method stub generated by customized ANTLR/Xtext generator.
	 * </p>
	 */
	@Override
	protected void setInRegularExpression() {
		if (!hasBufferedTokens()) {
			RegExLiteralAwareLexer lexer = (RegExLiteralAwareLexer) this.input.getTokenSource();
			lexer.setInRegularExpression();
		}
	}

	/**
	 * Assign the template mode {@link RegExLiteralAwareLexer lexer}. This method is called from
	 * {@link #ruleTemplateExpressionEnd()}. The method call is injected into the generated Antlr parser by
	 * {@link org.eclipse.n4js.antlr.n4js.TemplateLiteralDisambiguationInjector}, see MWE2 workflow for configuration.
	 * <p>
	 * Overrides method stub generated by customized ANTLR/Xtext generator.
	 * </p>
	 */
	@Override
	protected void setInTemplateSegment() {
		if (!hasBufferedTokens()) {
			RegExLiteralAwareLexer lexer = (RegExLiteralAwareLexer) this.input.getTokenSource();
			lexer.setInTemplateSegment();
		}
	}

	private boolean hasBufferedTokens() {
		return input.index() < input.size() - 1;
	}

	private boolean hasCurrentError() {
		try {
			return reflectCurrentError.get(this) != null;
		} catch (Exception e) {
			throw new RuntimeException();
		}
	}

	private void setCurrentError(SyntaxErrorMessage syntaxErrorMessage) {
		try {
			reflectCurrentError.set(this, syntaxErrorMessage);
		} catch (Exception e) {
			throw new RuntimeException();
		}
	}

	/**
	 * <p>
	 * Overrides method stub generated by customized ANTLR/Xtext generator.
	 * </p>
	 */
	@Override
	public void addASIMessage() {
		if (!hasCurrentError()) {
			SyntaxErrorMessage message = new SyntaxErrorMessage("Automatically inserted semicolon", SEMICOLON_INSERTED);
			setCurrentError(message);
		}
	}

	@Override
	public void discardError() {
		setCurrentError(null);
	}

	@Override
	public RecognizerSharedState getState() {
		return state;
	}

	@Override
	public void recoverBase(IntStream inputStream, RecognitionException re) {
		super.recover(inputStream, re);
	}

	@Override
	public RecoverySets getRecoverySets() {
		return recoverySets;
	}

	@Override
	public int getCommaBit() {
		return Comma;
	}

	/**
	 * Recover from an error found on the input stream. This is for {@link NoViableAltException} and
	 * {@link MismatchedTokenException}. If you enable single token insertion and deletion, this will usually not handle
	 * mismatched symbol exceptions but there could be a mismatched token that the {@link #match(IntStream, int, BitSet)
	 * match} routine could not recover from.
	 */
	@Override
	public void recover(IntStream inputStream, RecognitionException re) {
		SemicolonInjectionHelper.recover(inputStream, re, this);
	}

	// /**
	// * Simulate a semantic predicate that is executed before any token LA was used. This way we can make sure that a
	// * variable declaration without a type declaration does not mess the node model.
	// * <p>
	// * Overrides method stub generated by customized ANTLR/Xtext generator.
	// * </p>
	// */
	// @Override
	// protected boolean isTypeRefNoTrailingLineBreak() {
	// return SemicolonInjectionHelper.isTypeRefNoTrailingLineBreak(this);
	// }

	// @Override
	// public void tryRuleTypeRefNoTrailingLineBreak() throws RecognitionException {
	// ruleTypeRefNoTrailingLineBreak();
	// }

	@Override
	public BitSet getSemicolonFollowSet() {
		return FOLLOW_ruleExpression_in_ruleExpressionStatement;
	}

	/**
	 * <p>
	 * Overrides method stub generated by customized ANTLR/Xtext generator.
	 * </p>
	 */
	@Override
	protected boolean forcedRewind(int position) {
		input.seek(position);
		addASIMessage();
		return true;
	}

	@Override
	protected void appendTrailingHiddenTokens() {
		exhaustTokenSource();
		super.appendTrailingHiddenTokens();
	}

	private void exhaustTokenSource() {
		LazyTokenStream casted = (LazyTokenStream) this.input;
		int marked = casted.mark();
		try {
			while (casted.LT(1) != Token.EOF_TOKEN) {
				casted.consume();
			}
		} finally {
			casted.rewind(marked);
		}
	}

	/**
	 * Reports errors that should be present in the node model. This is specialized because we want real errors to
	 * override the synthesized diagnostics for automatically inserted semicolons.
	 *
	 * Usually only one syntax error message will be reported per location. For input documents of the form
	 *
	 * <pre>
	 * var i};
	 * </pre>
	 *
	 * we insert a semicolon automatically before the closing brace. That implies, that we have a message on the brace
	 * itself. The parser will try to match the real brace afterwards against the follow element set and fails. It tries
	 * to report that error but since there is already a message it would be discarded. Here we force the real error
	 * message to be replace the former info.
	 */
	@Override
	public void reportError(RecognitionException e) {
		if (state.errorRecovery) {
			return;
		}
		try {
			SyntaxErrorMessage currentError = (SyntaxErrorMessage) reflectCurrentError.get(this);
			if (currentError != null && SEMICOLON_INSERTED.equals(currentError.getIssueCode())) {
				setCurrentError(null);
			}
			super.reportError(e);
		} catch (IllegalArgumentException | IllegalAccessException e1) {
			super.reportError(e);
		}
	}

	/**
	 * <p>
	 * Promotes EOL which may lead to an automatically inserted semicolon. This is probably the most important method
	 * for automatic semicolon insertion, as it is only possible to insert a semicolon in case of line breaks (even if
	 * they are hidden in a multi-line comment!).
	 * </p>
	 * <p>
	 * Overrides method stub generated by customized ANTLR/Xtext generator.
	 * </p>
	 */
	@Override
	protected void promoteEOL() {
		SemicolonInjectionHelper.promoteEOL(this);
	}

	/**
	 * Overrides method stub generated by customized ANTLR/Xtext generator.
	 */
	@Override
	protected boolean hasDisallowedEOL() {
		return SemicolonInjectionHelper.hasDisallowedEOL(this);
	}

	/**
	 * Overrides method stub generated by customized ANTLR/Xtext generator.
	 */
	@Override
	protected boolean hasDisallowedWhiteSpace() {
		return SemicolonInjectionHelper.hasDisallowedWhiteSpace(this);
	}

	/** Added for debugging purposes only. */
	@SuppressWarnings("unused")
	private String bitsetName(BitSet bitset) {
		Optional<Field> findField = Stream.of(InternalN4JSParser.class.getDeclaredFields())
				.filter(f -> Modifier.isStatic(f.getModifiers())).filter(f -> {
					try {
						return bitset == f.get(null);
					} catch (Exception ex) {
						//
					}
					return false;
				}).findFirst();
		return findField.map(Field::getName).orElse("NN");
	}

	@Override
	public Object match(IntStream is, int ttype, BitSet follow) throws RecognitionException {
		LazyTokenStream lts = (LazyTokenStream) is;
		Token token = lts.LT(1);

		if (lts.forbidHiddenTokens && token != null
				&& token.getChannel() == Token.HIDDEN_CHANNEL) {

			Object matchedSymbol = getCurrentInputSymbol(input);
			if (state.backtracking > 0) {
				state.failed = true;
				return matchedSymbol;
			}
			matchedSymbol = recoverFromMismatchedToken(input, ttype, follow);
			return matchedSymbol;
		}
		return super.match(is, ttype, follow);
	}

}
